package com.aliyun.commons.dlock.impl;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import javax.annotation.Resource;

import com.aliyun.commons.dlock.DistributedLock;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

/**
 * 分布式锁实现
 *
 * 
 * @date 2024/11/28
 */
@Service
@Slf4j
public class DistributedLockImpl implements DistributedLock {

    private static final int SECONDS_TO_MILLS_RATIO = 1000;
    private static final int HALF_SECONDS_IN_MILLS = 5000;
    public static final Long SUCCESS_CODE = 1L;

    @Resource
    private RedisTemplate scriptRedisTemplate;
    @Resource(name = "delIfPresent")
    private DefaultRedisScript<Long> delIfPresent;
    @Resource(name = "setExIfAbsent")
    private DefaultRedisScript<Long> setExIfAbsent;
    @Resource(name = "setExIfPresent")
    private DefaultRedisScript<Long> setExIfPresent;

    private ScheduledExecutorService scheduledService = new ScheduledThreadPoolExecutor(1);

    @Override
    public <T> LockResult<T> executeWithLock(String lockKey, int expireSeconds, Supplier<T> action) {
        String lockValue = buildLockValue();
        try {
            // 加锁
            Boolean acquiredLock = this.lock(lockKey, lockValue, expireSeconds);
            if (!acquiredLock) {
                return new LockResult<>(false, null);
            }
            return new LockResult<>(true, action.get());
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            // 解锁
            this.unlock(lockKey, lockValue);
        }
    }

    /**
     * 构建锁定值
     *
     * @return {@link String }
     */
    private String buildLockValue() {
        return UUID.randomUUID() + "_" + RandomStringUtils.random(12, true, true);
    }

    /**
     * 锁
     *
     * @param lockName      锁名字
     * @param lockValue     锁值
     * @param expireSeconds 到期秒
     * @return boolean
     */
    private boolean lock(String lockName, String lockValue, int expireSeconds) {
        long expireMills = expireSeconds * SECONDS_TO_MILLS_RATIO;
        if (expireMills <= HALF_SECONDS_IN_MILLS) {
            throw new IllegalArgumentException("expireSeconds is invalid");
        }
        Long result = (Long)scriptRedisTemplate.execute(setExIfAbsent, Collections.singletonList(lockName), lockValue,
            expireSeconds);
        if (SUCCESS_CODE.equals(result)) {
            long delayMills = expireMills - HALF_SECONDS_IN_MILLS;
            delaySetExpireTime(lockName, lockValue, expireSeconds, delayMills);
            return true;
        }
        log.info("Lock acquisition failed: key={} value={} expireSeconds={}", lockName, lockValue, expireSeconds);
        return false;
    }

    /**
     * 设置过期时间
     *
     * @param lockName
     * @param lockValue     lock值
     * @param expireSeconds
     */
    private void setExpireTime(String lockName, String lockValue, int expireSeconds) {
        long delayMills = expireSeconds * SECONDS_TO_MILLS_RATIO - HALF_SECONDS_IN_MILLS;
        Long result = (Long)scriptRedisTemplate.execute(setExIfPresent, Collections.singletonList(lockName), lockValue,
            expireSeconds);
        if (SUCCESS_CODE.equals(result)) {
            delaySetExpireTime(lockName, lockValue, expireSeconds, delayMills);
            return;
        }
        log.info("Failed to set expiration time: the key '{}' does not exist", lockName);
    }

    /**
     * 延迟设置过期时间
     *
     * @param lockName      锁名字
     * @param lockValue     锁值
     * @param expireSeconds 到期秒
     * @param delayMills    延迟米尔斯
     */
    private void delaySetExpireTime(String lockName, String lockValue, int expireSeconds, long delayMills) {
        scheduledService.schedule(() -> setExpireTime(lockName, lockValue, expireSeconds), delayMills,
            TimeUnit.MILLISECONDS);
    }

    /**
     * 解锁
     *
     * @param lockName  锁名字
     * @param lockValue 锁值
     * @return boolean
     */
    private void unlock(String lockName, String lockValue) {
        int size = 0;
        Long result = null;
        do {
            try {
                result = (Long)scriptRedisTemplate.execute(delIfPresent, Collections.singletonList(lockName),
                    lockValue);
                break;
            } catch (Exception e) {
                size++;
                log.error(e.getMessage(), e);
            }
        } while (size <= 2);
        if (SUCCESS_CODE.equals(result)) {
            return;
        }
        log.warn("Failed to unlock, key={}", lockName);
    }
}
